Add solana-pay-request: build a Solana Pay transfer-request URL, no permissions - #156
Open
belumume wants to merge 2 commits into
Open
Add solana-pay-request: build a Solana Pay transfer-request URL, no permissions#156belumume wants to merge 2 commits into
belumume wants to merge 2 commits into
Conversation
Turns a payment intent into a Solana Pay transfer-request URL and a QR-ready payload a customer's wallet scans to pay. Given a recipient and an optional amount, SPL mint, reference keys, label, message and memo, it validates every field and builds a `solana:` URL faithful to the Solana Pay spec. The plugin declares `permissions = []`. It holds no key, reads no config and opens no socket, so a compromised copy has no I/O surface at all. The built component imports no `wasi:http`, no `wasi:sockets` and no `wasi:filesystem`, and the README gives a grep over the artifact, with a control, that lets a reader confirm that rather than take it on trust. Free text is the interesting part. `recipient`, `amount`, `spl-token` and `reference` each arrive in their own typed field and are validated before the URL is built, while `label`, `message` and `memo` are stripped of control, bidi and zero-width characters and then percent-encoded, so a memo reading `table 4&recipient=<attacker>&amount=999` becomes one encoded memo value instead of a second recipient. `src/solana_core/` is vendored from the plugin's source repository, named and dated in its own module header. A registry plugin builds from its directory alone, so a path dependency reaching outside it cannot resolve. `sanitize.rs` is verbatim; `pubkey.rs` is cut down to the base58 codec, which drops the curve25519, ed25519 and sha2 dependencies the omitted PDA derivation needed. 84 host tests pass with no wasm toolchain and no network: 80 unit tests over the request core and the vendored modules, plus 4 integration tests covering the URL the README documents, the injection case above, a rejected recipient, and the promise that `qr_payload` is byte-identical to `url`.
The header claimed sanitize.rs was verbatim. It differs from upstream by one module-doc paragraph, rewritten here because the original made a comparative claim about other plugins that a reader of this repo has no way to check. The logic is unchanged. Only the claim about the copy was wrong, and a reader who trusts it does not diff.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
solana-pay-request, a tool plugin that turns a payment intent into a Solana Pay transfer-request URL and a QR-ready payload a customer's wallet scans to pay.Given a recipient and an optional amount, SPL mint, reference keys, label, message and memo, it validates every field and builds a
solana:URL faithful to the Solana Pay spec. The agent-facing shape is one tool call: "charge table 4 for 25 USDC" comes back as a scannable URL.Custody:
permissions = []It holds no key, reads no config and opens no socket. That is not a claim about the source, it is a property of the component: a WASI interface can only be imported by name, so the name is in the binary if the capability is reachable. On this build:
The right-hand column is the control. Three zeros mean nothing unless the same command returns non-zero for interfaces that are genuinely present, and the README ships both loops so a reviewer can re-run them against the artifact they built rather than take the numbers from me.
Nothing it emits can move money on its own. The output is a payment request; the payer's wallet builds and signs the transaction.
Free text cannot restructure the URL
recipient,amount,spl-tokenandreferenceeach arrive in their own typed field and are validated before the URL is built.label,messageandmemoare stripped of control, bidi and zero-width characters, then percent-encoded at build time. A memo readingtable 4&recipient=<attacker>&amount=999becomes one encoded memo value rather than a second recipient, which is pinned as an integration test.Vendoring
src/solana_core/is copied from the plugin's source repository, with the repository, revision and file paths named in its module header.Vendoring is deliberate rather than convenient. A plugin here is built from its own directory plus
wit/, so a path dependency reaching outside that directory cannot resolve, and the crate it would name is unpublished.sanitize.rsis verbatim.pubkey.rsis cut down to the base58 codec, which drops thecurve25519-dalek,ed25519-dalekandsha2dependencies that only the omitted PDA and on-curve code needed; the reduction is recorded in the file so a future need for derivation pulls the upstream module back whole instead of regrowing it by hand.bs58is the one dependency added beyond the template's.What I ran
Against this branch, from
plugins/solana-pay-request/:cargo test --lockedcargo clippy --locked --all-targets -- -D warningscargo clippy --locked --target wasm32-wasip2 -- -D warningscargo build --locked --target wasm32-wasip2 --releasecargo fmt --all -- --checkgit diff --check origin/main HEADtools/build-registry.py --source-plugins plugins --check-metadata registry.jsonpending unpublished source: solana-pay-request@0.1.0tools/ci/validate_components.sh solana-pay-requestwas also run directly, so the four cargo commands above were exercised through the snapshot the workflow actually builds in rather than in a working tree that happened to have more in it.Two caveats, stated rather than left for CI to surface. The
tools/testsandtools/ci/testssuites report failures on my machine, but they fail identically on an unmodified checkout ofmainand every failing case is a symlink or non-regular-file test, which is a Windows limitation rather than anything this branch touches. Andregistry.jsonis untouched, since it is generated.Fit
The registry currently indexes one
toolplugin against thirtychannelplugins. This is a tool plugin with no permissions at all, which makes it about the cheapest possible thing to review and install, and it is useful to any agent on any of those channels: taking a payment is not chain-specific plumbing, it is the thing the operator wanted the agent for.Happy to split the vendored module out, rename, or adjust the manifest if any of it cuts against how you would rather see third-party plugins land here.
Validation
CI does not run on pull requests from forks in this repo, so here is the output of your own
validator, unmodified, on this branch:
Reproduce with:
Also clean:
cargo fmt --check,git diff --checkagainst main, and--check-metadata, whichreports
pending unpublished source: solana-pay-request@0.1.0.The manifest declares
permissions = []. That is verified against the built artifact rather thanthe source:
wasi:http,wasi:socketsandwasi:filesystemeach appear 0 times in the component,with
wasi:cliat 23,wasi:ioat 11 andzeroclaw:pluginat 15 confirming the check reads thebinary.
Two limits worth stating rather than leaving you to find. It was built on Rust 1.97.1, not the
1.96.1 your workflow pins, so that combination is untested by me. And it has not been built on
Linux; your
tools/testsandtools/ci/testssuites fail on my machine, but they failbyte-identically on an unmodified checkout of
mainand every failing case is a symlink test, sothat looks pre-existing and platform-specific rather than related to this change.